import functools
import inspect
import json
import time
import six
import traceback

from squirrel_core.commons.database.db_manager import DatabaseManager
from squirrel_core.commons.utils.logger import Logging
from squirrel_core.commons.utils.tools import walk_modules

__all__ = ["get_provider_classes", "get_provider", "DbProvider"]


def get_provider_classes(paths=None):
    module_paths = []
    module_paths.append(__package__)
    if isinstance(paths, (list, tuple)):
        module_paths.extend(paths)
    elif isinstance(paths, (str,)):
        module_paths.append(paths)
    exceptions = []
    for path in module_paths:
        for _module in walk_modules(path):
            try:
                _module = inspect.importlib.reload(_module)
                for obj in six.itervalues(vars(_module)):
                    if inspect.isclass(obj) and issubclass(obj, (DbProvider,)):
                        yield obj
            except Exception as err:
                exceptions.append(err)


def get_provider(provider_type, *args, **kwargs):
    logger = kwargs.pop('logger', '') if kwargs.get('logger', '') else Logging()
    classes = get_provider_classes()
    mapping = {}
    if 'timeout' not in kwargs.get('db_setting', {}):
        kwargs['db_setting']['timeout'] = 30
    for _class in classes:
        try:
            if _class.__name__ == DbProvider.__name__:
                continue
            provider_name = getattr(_class, "provider_name")
            provider_name = provider_name.fget(None)
            if provider_name:
                mapping.update({
                    provider_name: _class
                })
        except Exception:
            err_msg = traceback.format_exc()
            logger.info(f"DB provider {_class} don't have a valid provider name, error:\r\n{err_msg}")

    if provider_type in mapping.keys():
        return mapping[provider_type](logger, *args, **kwargs)
    else:
        raise Exception(f"Cannot find DB provider for db type {provider_type}")


class DbProvider(object):
    def __init__(self, logger, *args, **kwargs):
        self._connection = DatabaseManager(self.provider_name, *args, **kwargs).connect()
        self.logger = logger
        self.db_reconnect_retry = kwargs.get("db_reconnect_retry", 0)
        self.sleep_time_once_failed = kwargs.get("sleep_time_once_failed", 0)
        
    def __repr__(self):
        return '{}'.format(self._connection)

    @property
    def connection(self):
        return self._connection

    def _execute(self, cmd, *args, **kwargs):
        inner_cmd = "_" + cmd
        if inner_cmd not in dir(self):
            raise Exception("Cmd %s is not supported" % cmd)
        _cmd = getattr(self, inner_cmd)
        return _cmd(*args, **kwargs)

    def __getattr__(self, cmd):
        if cmd not in self.__dict__:
            self.__dict__[cmd] = functools.partial(self.execute, cmd)
        return self.__dict__[cmd]

    def execute(self, cmd, *args, **kwargs):
        for retry_times in range(0, self.db_reconnect_retry + 1):
            try:
                ret = self._execute(cmd, *args, **kwargs)
                return ret
            except Exception as err:
                msg = {
                    'content': 'Command execute failed!',
                    'error': traceback.format_exc(),
                    'cmd': cmd,
                    'args': args,
                    'retry_times': retry_times
                }
                self.logger.warning(json.dumps(msg))
                if retry_times == self.db_reconnect_retry:
                    raise err

            time.sleep(self.sleep_time_once_failed)

    def _close(self):
        raise NotImplementedError()

    def _queue_push_back(self, key, value, *args, **kwargs):
        raise NotImplementedError()

    def _queue_pop_back(self, key, *args, **kwargs):
        raise NotImplementedError()

    def _queue_push_front(self, key, value, *args, **kwargs):
        raise NotImplementedError()

    def _queue_pop_front(self, key, *args, **kwargs):
        raise NotImplementedError()

    def _queue_push(self, key, value, *args, **kwargs):
        raise NotImplementedError()

    def _queue_pop(self, key, *args, **kwargs):
        raise NotImplementedError()

    def _queue_size(self, key, *args, **kwargs):
        raise NotImplementedError

    def _queue_clear(self, key, *args, **kwargs):
        raise NotImplementedError()

    def _set_pop(self, key, *args, **kwargs):
        raise NotImplementedError

    def _set_del(self, name, key, *args, **kwargs):
        raise NotImplementedError

    def _set_get(self, name, key, *args, **kwargs):
        raise NotImplementedError

    def _set_exists(self, name, key, *args, **kwargs):
        raise NotImplementedError

    def _set_add(self, key, value, *args, **kwargs):
        raise NotImplementedError()

    def _set_size(self, key, *args, **kwargs):
        raise NotImplementedError

    def _set_clear(self, key, *args, **kwargs):
        raise NotImplementedError()

    def _set_keys(self, name, *args, **kwargs):
        raise NotImplementedError()

    def _set_incr(self, name, key, num=1, *args, **kwargs):
        raise NotImplementedError()

    def _hash_clear(self, name, *args, **kwargs):
        raise NotImplementedError()

    def _hash_add(self, name, key, value, *args, **kwargs):
        raise NotImplementedError()

    def _hash_get(self, name, key, *args, **kwargs):
        raise NotImplementedError()

    @property
    def provider_name(self):
        raise NotImplementedError()

    def save(self, item_dict, *args, **kwargs):
        self.process_item(item_dict, *args, **kwargs)
        status = False
        for retry_times in range(0, self.db_reconnect_retry + 1):
            status = self._save(item_dict, *args, **kwargs)
            if status:
                break
            time.sleep(self.sleep_time_once_failed)
        return status

    def _save(self, item_dict, *args, **kwargs):
        raise NotImplementedError()

    def log_msg(self, status, item_dict, err_msg=None):
        msg = {}
        if status:
            msg.update({'msg': 'Item save successful!'})
        else:
            msg.update({'msg': 'Item save failed!'})
            msg.update({'error': err_msg})
        self.logger.info(json.dumps(msg))

    def process_item(self, item_dict, *args, **kwargs):
        pass
